package com.feature50.makeover;

import com.feature50.makeover.utils.ImageUtils;
import com.feature50.makeover.utils.ResourceLoader;
import com.feature50.makeover.utils.StringUtils;
import com.feature50.makeover.whackachet.ChetPanel;
import com.jgoodies.forms.factories.Borders;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import org.apache.log4j.Logger;
import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.painter.gradient.BasicGradientPainter;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

import javax.swing.*;
import javax.swing.FocusManager;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.Point2D;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

public class DesktopFrame extends JFrame {
    private static final Logger logger = Logger.getLogger(DesktopFrame.class);

    public static final Icon ICON_APP_DEFAULT = ImageUtils.getIcon("icn_application.png");

    private ButtonTabPanel buttonTabPanel;

    private Animator showAnimator;
    private Animator hideAnimator;
    private NavigationPanel panel;
    private JToggleButton nav;

    private JPanel popupPanel;

    public DesktopFrame() throws HeadlessException {
        super("Extreme GUI Makeover");
        init();
    }

    private void init() {
        JPanel mainPanel = new JPanel();

        FormLayout mainLayout = new FormLayout("fill:default:grow", "default, fill:default:grow");
        mainPanel.setLayout(mainLayout);

        JPanel formPanel = new JPanel();

        buttonTabPanel = new ButtonTabPanel(formPanel);
        mainPanel.add(buttonTabPanel, new CellConstraints().xy(1, 1));

        nav = new JToggleButton();
        decorateButton(nav, "navicn_filelist");
        buttonTabPanel.addButton(nav);
        nav.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (nav.isSelected()) {
                    showNavigationPane();
                } else {
                    hideNavigationPane();
                }
            }
        });

        JButton prefs = new JButton();
        decorateButton(prefs, "navicn_prefs");
        buttonTabPanel.addButton(prefs);

        prefs.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                JPanel panel = new JPanel();
                panel.setPreferredSize(new Dimension(500, 400));
                panel.setOpaque(false);
                displayPopup(panel, "Test");
            }
        });

        JButton bug = new JButton();
        decorateButton(bug, "navicn_bugs");
        buttonTabPanel.addButton(bug);

        formPanel.setBackground(Color.BLACK);
        mainPanel.add(formPanel, new CellConstraints().xy(1, 2));

        getContentPane().add(mainPanel);

        panel = new NavigationPanel(this);
        panel.setOpaque(false);

        mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "navigation");
        mainPanel.getActionMap().put("navigation", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                nav.doClick();
            }
        });

        retrieveNavNodes();
    }

    public void launchNavApp(ENode node) {
        if (node.getLauncher() == null) return;

        showApplication(node);
        panel.addRecentApp(node);
        hideNavigation();
    }

    private void retrieveNavNodes() {
        // in the real application, loading navigation nodes is expensive. in this demo app, spawning up the background
        // thread is likely unnecessary but I'm keeping it to save a few ms. in UI init'ing
        new Thread() {
            public void run() {
                try {
                    InputStream ns = ResourceLoader.getInstance().getInputStream(ResourceLoader.Type.None, "nodes.xml");
                    final Document doc = new SAXBuilder().build(ns);
                    ns.close();

                    final TreeModel model = new TreeModel() {
                        // equality is based on object identity in this demo; these maps allow reuse of ENodes
                        private Map<Element, ENode> enodes = new HashMap<Element, ENode>();
                        private Map<ENode, Element> elements = new HashMap<ENode, Element>();

                        public Object getRoot() {
                            return getNode(doc.getRootElement());
                        }

                        public Object getChild(Object parent, int index) {
                            Element xp = elements.get(parent);
                            return getNode((Element) xp.getChildren().get(index));
                        }

                        public int getChildCount(Object parent) {
                            Element xp = elements.get(parent);
                            return xp.getChildren().size();
                        }

                        public boolean isLeaf(Object node) {
                            Element xp = elements.get(node);
                            return xp.getChildren().isEmpty();
                        }

                        public void valueForPathChanged(TreePath path, Object newValue) {}

                        public int getIndexOfChild(Object parent, Object child) {
                            Element xp = elements.get(parent);
                            return xp.getChildren().indexOf(elements.get(child));
                        }

                        public void addTreeModelListener(TreeModelListener l) {}
                        public void removeTreeModelListener(TreeModelListener l) {}

                        private ENode getNode(Element e) {
                            ENode node = enodes.get(e);
                            if (node == null) {
                                // hard-coded demo launchers
                                AppLauncher launcher = getAppLauncher(e);
                                node = new ENode(enodes.get(e.getParentElement()), e.getTextTrim(), launcher);
                                enodes.put(e, node);
                                elements.put(node, e);
                            }
                            return node;
                        }
                    };

                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            panel.setTreeModel(model);
                        }
                    });
                } catch (Exception e) {
                    logger.error("Couldn't retrieve nav nodes", e);
                }
            }
        }.start();
    }

    /**
     * Returns a hard-coded launcher for the extreme demo.
     *
     * @param e
     * @return
     */
    private AppLauncher getAppLauncher(Element e) {
        String id = e.getAttributeValue("id");
        if (StringUtils.nullOrEmpty(id)) return new AppLauncher() {
            public JComponent launchApplication(ENode node) {
                return new DefaultPanel();
            }
        };

        if (id.equals("dashboard")) {
            return new AppLauncher() {
                public JComponent launchApplication(ENode node) {
                    return new DashboardPanel();
                }
            };
        } else if (id.equals("chet")) {
            return new AppLauncher() {
                public JComponent launchApplication(ENode node) {
                    return new ChetPanel();
                }
            };
        }

        return null;
    }

    public void doLayout() {
        super.doLayout();

        int y = (int) buttonTabPanel.getBounds().getHeight();

        panel.setBounds(0, y, 330, getHeight() - (y * 2));
        panel.revalidate();
    }

    public void closeActiveApplication() {}

    public String[] getApplicationNames() {
        // todo: implement this
        return new String[0];
    }

    public Icon getNodeIcon(ENode node) {
        return ICON_APP_DEFAULT;
    }

    public void showApplication(ENode node) {
        JComponent appComponent = node.getLauncher().launchApplication(node);

        final ButtonTabPanel.TabButton tabButton = buttonTabPanel.addTabPanel(node.getDisplay(), appComponent, true);
        tabButton.addCloseActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                buttonTabPanel.removeTabPanel(tabButton);
            }
        });
    }

    private void decorateButton(AbstractButton button, String image) {
        button.setIcon(ImageUtils.getIcon(image + "_off.png"));

        Icon icon = ImageUtils.getIcon(image + "_on.png");
        button.setSelectedIcon(icon);
        button.setPressedIcon(icon);

        button.setContentAreaFilled(false);
        button.setFocusable(false);
        button.setBorder(null);
        button.setOpaque(false);
    }

    public void ensureNavigationVisible() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if (!nav.isSelected()) {
                    nav.doClick();
                }
            }
        });
    }

    public void hideNavigation() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if (nav.isSelected()) nav.doClick();
            }
        });
    }

    private void showNavigationPane() {
        cancelAnimation();

        showAnimator = new Animator(750, new TimingTargetAdapter() {
            public void begin() {
                JLayeredPane layeredPane = getLayeredPane();

                // remove the panel as a sanity check to ensure it's no longer present
                layeredPane.remove(panel);

                layeredPane.add(panel, JLayeredPane.PALETTE_LAYER);
                layeredPane.revalidate();
                layeredPane.repaint();

                panel.resetImages();
            }

            public void timingEvent(float v) {
                panel.setBuilt(v);
                panel.repaint();
            }

            public void end() {
                panel.setBuilt(1f);
                panel.revalidate();
                panel.repaint();

                panel.setFocusToQuickFind();
            }
        });
        showAnimator.setDeceleration(0.8f);
        showAnimator.setStartFraction(panel.getBuilt());
        showAnimator.start();
    }

    private void removeNavFocus() {
        Component comp = FocusManager.getCurrentManager().getFocusOwner();
        if ((SwingUtilities.isDescendingFrom(comp, panel)) || (comp == null)) {
            buttonTabPanel.requestFocus();
        }
    }

    private void cancelAnimation() {
        if ((showAnimator != null) && (showAnimator.isRunning())) showAnimator.cancel();
        if ((hideAnimator != null) && (hideAnimator.isRunning())) hideAnimator.cancel();
    }

    private void hideNavigationPane() {
        cancelAnimation();

        hideAnimator = new Animator(750, new TimingTargetAdapter() {
            public void begin() {
                panel.resetImages();
                removeNavFocus();
            }

            public void timingEvent(float v) {
                panel.setBuilt(v);
                panel.repaint();
            }

            public void end() {
                panel.setBuilt(0f);

                JLayeredPane layeredPane = getLayeredPane();
                layeredPane.remove(panel);
                layeredPane.revalidate();
                layeredPane.repaint();
            }
        });
        hideAnimator.setAcceleration(0.7f);
        hideAnimator.setStartFraction(panel.getBuilt());
        hideAnimator.setStartDirection(Animator.Direction.BACKWARD);
        hideAnimator.start();
    }

    public void hidePopup() {
        getLayeredPane().remove(popupPanel);
        getLayeredPane().revalidate();
        getLayeredPane().repaint();

        popupPanel = null;
    }

    public void displayPopup(JPanel contents, String title) {
        CellConstraints CC = new CellConstraints();

        // modal hierarchy:
        // popupPanel -> popPanel -> modalPanel -> gradientPanel -> contents

        JPanel modalPanel = new JPanel();
        modalPanel.setLayout(new FormLayout("fill:default:grow", "default, fill:default:grow"));
        modalPanel.add(new JLabel(title), CC.xy(1, 1));

        final BouncyLightweightPopup popPanel = new BouncyLightweightPopup(modalPanel);

        final float[] built = new float[1];
        popupPanel = new JPanel() {
            protected void paintComponent(Graphics g) {
                g.setColor(new Color(90, 90, 90, (int) (220f * built[0])));
                g.fillRect(0, 0, getWidth(), getHeight());
            }
        };

        popupPanel.setLayout(new FormLayout("center:default:grow", "center:default:grow"));
        popupPanel.setOpaque(false);
        popupPanel.add(popPanel, CC.xy(1, 1));
        popPanel.setBuilt(0f);

        JXPanel gradientPanel = new JXPanel();
        gradientPanel.setBackgroundPainter(new BasicGradientPainter(new Point2D.Double(0, 0), new Color(5, 5, 5), new Point2D.Double(0, 1f), new Color(27, 30, 37)));
        gradientPanel.setBorder(Borders.DLU4_BORDER);
        gradientPanel.setLayout(new GridLayout(1, 1));
        modalPanel.add(gradientPanel, CC.xy(1, 2));
        gradientPanel.add(contents);

        modalPanel.setBorder(BorderFactory.createMatteBorder(0, 1, 1, 1, Color.BLACK));

        JLayeredPane layeredPane = getLayeredPane();
        layeredPane.setLayer(popupPanel, 350);
        layeredPane.add(popupPanel);
        popupPanel.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight());
        layeredPane.revalidate();
        layeredPane.repaint();

        Animator animator = new Animator(175, new TimingTargetAdapter() {
            public void begin() {
                popPanel.setBuilt(0f);
                built[0] = 0f;
                popupPanel.repaint();
            }

            public void timingEvent(float v) {
                popPanel.setBuilt(v);
                built[0] = v;
                popupPanel.repaint();
            }

            public void end() {
                popPanel.setBuilt(1f);
                built[0] = 1f;
                popupPanel.revalidate();
                popupPanel.repaint();

                Animator nested = new Animator(80, new TimingTargetAdapter() {
                    float base = 1f - BouncyLightweightPopup.SIZE_REDUCER;

                    @Override
                    public void timingEvent(float fraction) {
                        float diff = BouncyLightweightPopup.SIZE_REDUCER;
                        float buildAmount = diff * fraction;

                        popPanel.setBuilt(base + buildAmount);
                        popPanel.repaint();
                    }

                    @Override
                    public void begin() {
                        popPanel.setBuilt(1f);
                        popupPanel.repaint();
                    }

                    @Override
                    public void end() {
                        popPanel.setBuilt(base);
                        popPanel.setBuildDone(true);
                        popupPanel.revalidate();
                        popupPanel.repaint();
                    }
                });
                nested.setStartDirection(Animator.Direction.BACKWARD);
                nested.setStartFraction(1.0f);
                nested.setDeceleration(0.8f);
                nested.start();
            }
        });
        animator.start();
    }
}